package com.happy3w.task.composer;

import java.lang.reflect.AnnotatedType;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Parameter;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;

public class AnnotationTaskDetector {
    public static List<Task> detectTasks(Object... taskHolders) {
        List<Task> tasks = new ArrayList<>();
        for (Object taskHolder : taskHolders) {
            collectTasks(taskHolder, tasks);
        }
        return tasks;
    }

    public static void collectTasks(Object taskHolder, List<Task> tasks) {
        if (taskHolder instanceof Class) {
            collectTasksFromType(null, (Class)taskHolder, tasks);
        } else {
            collectTasksFromType(taskHolder, taskHolder.getClass(), tasks);
        }
    }

    private static void collectTasksFromType(Object taskHolder, Class taskHolderType, List<Task> tasks) {
        boolean needStatic = taskHolder == null;
        for (Method method : taskHolderType.getDeclaredMethods()) {
            boolean isStatic = (method.getModifiers() & Modifier.STATIC) > 0;
            if (needStatic != isStatic) {
                continue;
            }

            List<TaskDataDef> outputs = collectOutputs(method);
            if (outputs == null || outputs.isEmpty()) {
                continue;
            }
            List<TaskDataDef> inputs = collectInputs(method);
            Task task = MethodTask.from(method, taskHolder, inputs, outputs);
            tasks.add(task);
        }
    }

    private static List<TaskDataDef> collectInputs(Method method) {
        List<TaskDataDef> inputs = collectInputsByAnnTypes(method.getAnnotatedParameterTypes());
        if (inputs == null || inputs.isEmpty()) {
            inputs = collectInputsByParams(method.getParameters());
        }
        return inputs;
    }

    private static List<TaskDataDef> collectInputsByAnnTypes(AnnotatedType[] annotatedParameterTypes) {
        List<TaskDataDef> taskDataDefs = new ArrayList<>();
        for (AnnotatedType annotatedType : annotatedParameterTypes) {
            collectTaskDatas(annotatedType, taskDataDefs);
        }
        return taskDataDefs;
    }

    private static List<TaskDataDef> collectInputsByParams(Parameter[] parameters) {
        List<TaskDataDef> taskDataDefs = new ArrayList<>();
        for (Parameter parameter : parameters) {
            collectTaskDatas(parameter.getAnnotation(TaskData.class),
                    parameter.getType(),
                    parameter.getAnnotation(TaskDatas.class),
                    taskDataDefs);
        }
        return taskDataDefs;
    }

    private static List<TaskDataDef> collectOutputs(Method method) {
        AnnotatedType annotatedReturnType = method.getAnnotatedReturnType();

        List<TaskDataDef> taskDataDefs = new ArrayList<>();
        collectTaskDatas(taskDataDefs, annotatedReturnType.getType(),
                annotatedReturnType.getAnnotation(TaskData.class),
                annotatedReturnType.getAnnotation(TaskDatas.class),
                method.getAnnotation(TaskData.class),
                method.getAnnotation(TaskDatas.class));
        return taskDataDefs;
    }

    private static void collectTaskDatas(List<TaskDataDef> taskDataDefs, Type taskType, Object... anns) {
        for (Object objAnn : anns) {
            if (objAnn == null) {
                continue;
            } else if (objAnn instanceof TaskData) {
                TaskData taskData = (TaskData) objAnn;
                taskDataDefs.add(new TaskDataDef(taskData.value(), taskType));
            } else if (objAnn instanceof TaskDatas) {
                TaskDatas taskDatas = (TaskDatas) objAnn;
                for (TaskData o : taskDatas.value()) {
                    taskDataDefs.add(new TaskDataDef(o.value(), Object.class));
                }
            }
        }
    }

    private static void collectTaskDatas(AnnotatedType annotatedType, List<TaskDataDef> taskDataDefs) {
        collectTaskDatas(taskDataDefs, annotatedType.getType(),
                annotatedType.getAnnotation(TaskData.class),
                annotatedType.getAnnotation(TaskDatas.class));
    }

    private static void collectTaskDatas(TaskData ann, Class dataType, TaskDatas anns, List<TaskDataDef> taskDataDefs) {
        if (ann != null) {
            taskDataDefs.add(new TaskDataDef(ann.value(), dataType));
        }

        if (anns != null) {
            for (TaskData o : anns.value()) {
                taskDataDefs.add(new TaskDataDef(o.value(), Object.class));
            }
        }
    }
}
